Web 应用高并发的思考与实践

Web 应用高并发的思考与实践

一、前言

2020 年初,突如其来的疫情使各行各业都进入紧张状态,互联网产品也各尽其能投入到战“疫”中。健康信息收集是抗疫过程中最基础的工作之一,腾讯问卷平台结合其信息采集、统计分析的特性,快速迭代出“健康打卡”“复学码”等功能。

然而,健康码关系着用户能否正常进出社区、公司或校园,计算的时效性和准确性必须有保证。加之用户量大、打卡时间点集中,在开发时间和资源都紧张的情况下,我们是如何应对访问高并发、数据量陡增,从而保障系统的稳定?

图 1

图 1

本文结合腾讯问卷后台的优化和改造工作,记录过程中的思考,整理提升 Web 应用负载能力的关注点。作为一个刚接触海量用户场景的小白,希望借此跟大家交流探讨。

图 2

图 2

实践围绕图2的四个点,从被动到主动,从发现到治理。这几个方法远远谈不上全面,但可能是短时间内性价比最高的实践。

 

二、思考

1. 理解和关注高并发

1.1 What & Why

“高并发(High Concurrency)通常是指,通过设计保证系统能够同时并行处理很多请求”……Web 应用需求变更快,需要持续改进、持续运营以应对潜在的用户量突增。

Web 应用负载能力不足会有怎样的表现?

  • 速度慢

比如打开页面持续 loading/白屏,需要等待或者重新进入,给用户带来很差的使用体验。

  • 系统奔溃

系统崩溃可能表现在服务无响应、组件不可用、用户数据丢失等。在问卷系统中假设用户提交的数据丢失,健康码会因为中断打卡由绿变红,给用户带来困扰,同时也给开发运维带来修复的工作量。

所幸通过项目组的合力改造,系统的抗压能力提升了一个数量级。但也引发了新的思考,假如我们的系统要支持更高的并发,如何复盘当前的工作,以及准备下一步的工作?

图3借用《海量运维、运营规划之道》一书中的负载与架构演进图:

图 3

图 3

作者认为,Web 架构的生命周期要求我们要杜绝过度设计,又要保证前瞻性。而在实践中我也形成了一种认知,高并发是相对而不是绝对的,在业务每个阶段都需要有敏捷支持相对于前一阶段更高负载的能力,做性价比最高的事情。

1.2 Who

作为应用开发我也曾陷入这样的误区,认为高并发是架构设计师、运维工程师才需要考虑的事情,实践中发现项目组的各个角色都可以为高并发能力做点什么。

  • 产品策划和交互设计

当一个功能被设计出来时,就可以预见是否存在高并发瓶颈,通过一些产品策略或者交互方式的转变,往往能缓解问题。

图 4

图 4

比如在“健康打卡”这个功能点,以往设计了在每天早上七点推送提醒,无形中给系统制造了高并发的场景,实际上推送的时间点可以交给客户设置、并且分散到不同时间段,从而合理利用机器资源。

  • 开发人员

即使架构师设计了良好的分布式方案、运维工程师保障了系统能稳定扩容,负载的瓶颈还会是被开发人员“制造”出来(更别提在我们的业务每个人都身兼数职)。后续的案例也可以体现开发在支持高并发过程中的职责。

2. 理解和关注业务

2.1 When

什么时候需要重点关注业务的并发能力?最好的时间点当然是在设计之初,但往往我们是中途加入项目,下表中这几个时机可以作为参考:

软件熵这里可以做一个简单的科普,熵是一个来自物理学的概念,指的是某个系统中的 “无序” 的总量,当软件中的无序增长时,我们称之为“软件腐烂”。

图5中举了个例子,问卷中有个打卡统计功能,在开发的初期只需要展示谁没有打卡,重实现轻设计。在逐渐新增交叉分析、筛选等功能,以及在修过几次统计不准确的 Bug 后,打卡统计越来越耗性能,以致于在并发量高时耗尽计算资源而挂掉。

图 5

图 5

所有的问题都是建立在历史问题的基础上,往往这个时候对问题的处理就没有第一个版本那么完美。实践的经验是代码开始腐烂的位置往往也是存在高并发风险的位置。这时候就需要尽早关注。

2.2 Where

“设计一个大型高并发网站的架构,首先必须以了解业务特点作为出发点,架构的目的是支撑业务”……

Web 应用的架构往往并不复杂,在项目中我们主要关注两个方面:

1)流量链路

  • 负载均衡
  • 网络连通

图 6

图 6

2)业务架构

  • 组件的选型是否合理
  • 依赖的服务是否稳定

图 7

图 7

图7粗略展示了腾讯问卷的技术栈,实践中其中某一个环节出问题都可能导致负载提升不上去,后文也将围绕各个环节来做分析。

三、实践

1. 洞察

1.1 监控

监控是为了第一时间发现、定位并记录异常。当前腾讯问卷系统虽谈不上立体化,但也在不断完善,保证了从业务层、系统层、网络层都有迹可循。

图 8

图 8

1.1.1 监控的指标选择

监控时我们往往最关注两个内容:时间和行为(路径),例如在应用层,左图记录了任务执行的耗时,右图记录了在任务执行过程中经历了哪些事件,二者结合便于判断性能的瓶颈、或者 bug 潜在的位置。同理对前端、对接入层也用了类似的指标。

图 9

图 9

1.1.2 监控的呈现

接下来是把指标可视化,将关键日志进行聚合,利用 Dashboard 呈现。

图 10

图 10

1.1.3 监控的监控

在异常爆发时,自建的监控系统在也会接收大量的上报,如果监控系统自己都挂掉了,对业务的监控也就失去意义,因此对监控系统也需要做保护,例如对错误上报做限流、对监控做额外的健康探测等。

1.2 压测

1.2.1 预测

  • 预估可能存在的瓶颈位

各个位置的带宽是否跑满?

组件的 CPU 或者内存使用率是过高?

业务机的 CPU 或者是磁盘 IO 是否跑满?

 

  • 预设目标

需求角度出发:实际总用户量可能是多少?同时会有多少人使用(打卡)功能?

现状角度出发:想达到的目标是当前负载能力的 5 倍?10 倍?

1.2.2 实施

我们常会使用 apache bench(简称ab) 测试接口,但到业务场景中,单接口的压测有局限性。在测试同学的带领下我们尝试了 Locust。

  • 如何模拟用户并发

 

首先要理解一个用户同时发起一万次请求,跟一万个用户同时发起一次请求,对系统造成的压力是不一样的。实测过程中我们预留了一千万个 ID 用于模拟真实用户,同时避免测试数据难以清理。

图 11

图 11

  • 如何模拟行为并发

这里先介绍一个用户使用问卷进行打卡的流程:打开问卷链接->登录->答题->提交答案。也就是说一个打卡场景实际涉及多个请求,在做压测的时候我们尽可能还原。在写完每个接口的测试方法后,Locust 可以使用 TaskSet 进行任务组合,并且支持权重配比。这个比例正好可以从我们的流量监控获得。

 

 

图 12

图 12

1.2.3 定位瓶颈

压测的目的是在于找到系统的瓶颈,一定是要确定系统某个方面达到瓶颈了,压力测试才算是基本完成…

  • 纵向:按流量链路逐层“断点” 回顾系统的链路,流量从前端到接入层、再到应用层、缓存层、存储层,每个环节都有被“撑爆”风险。只有逐层排查和改造,才能了解到链路中潜在的瓶颈位。

案例 1. 流量被前端拦住,并没有到后台

图 13

图 13

案例 2. 缓存层带宽不足

图 14

图 14

  • 横向:对照实验 经过逐层压测,系统的性能瓶颈定位到 MySQL 上,这也符合我们对业务的认知,因为系统重度依赖数据库并且缺乏针对性的优化。但是数据库的读写来源有接口服务和任务队列,我们采用了对照实验,调整 fpm worker 与 queue worker 数量比例,表格记录每一轮测试的效果,进而判断是哪个影响更大

1.2.4 压测小结

压力测试不仅是孤立测试各个软硬件的性能指标,而是要尽可能模拟真实的业务场景,从而充分评估系统上线后可能发生的情况,同时也为我们后续制定问题应对措施、制定改造计划提供了依据。

2. 响应

2.1 扩容

当用户量开始增多,现有系统无法处理那么大访问流量,最快速的响应方案是扩容。但是你的系统真的能快速扩容吗?

2.1.1 扩容的方式

  • 垂直扩展

软件上,通过调参能否充分利用硬件资源

硬件上,是否有升配的能力

  • 水平扩展

业务服务器加机器

组件集群加节点/分片

2.1.2 怎样支持扩容

对于老业务,我们往往要先解决怎样支持扩容的问题。

  • 解耦 业务中依赖了大量的公司内部服务,但由于网络策略或安全因素,它们影响了业务的扩容能力。实践中我们把这些依赖剥离出来,通过代理等形式,形成了局部单体、整体可扩容的架构。
  • 上云 以往的单体应用架构制约了扩容能力:比如需要在应用层加一台物理机,需要从装系统开始做起;给 MongoDB 加一个节点,需要大量的运维操作。借助云服务的灵活性可以大大提高系统的扩容能力。

腾讯问卷上云主要包括以下三个方面:

在上云过程中多多少少会踩坑,这里暂不展开讨论,后续再开新篇深度记录。

2.1.3 扩容还需要注意什么

  • 依赖的组件是否会扛不住
  • 依赖的远程服务是否会因为扩容被拖垮

在压测过程中可能会在测试环境 Mock 掉一些远程请求,导致在线上扩容业务服务后,问题才暴露出来,需要引以为戒。

图 15

图 15

显示了在并发量逐步上升的过程中,QPS 出现下降再上升的情况,排查发现是依赖的远程服务容量不足导致的性能下降,所以在压测过程中不能仅关注系统本身。

2.2 优化

扩容往往只是一种缓解措施,从运维的角度提升了系统的负载能力,但随之而来的是服务器成本的上升,我们需要通过改造和优化使资源的利用更合理。

2.2.1 关注代码逻辑

  • 慢执行

为了功能快速上线,我们往往忽略了代码里耗时的方法和逻辑,当大量请求涌入,慢执行就会阻塞后续的请求,拖慢系统的其他接口,甚至造成雪崩。

CPU 密集型的比如 web 应用常会防止 xss 攻击,一般会用正则过滤标签,在面对大文本、高并发、或者是循环的时候,都会大量占用 CPU,使业务服务器的压力上升。

IO 密集型的比如上传下载文件时过度扫描目录,任务的大部分时间都在等待 IO 操作完成,响应时间变长,这时候业务服务器的 CPU 使用率不高,而磁盘读写压力大。

对于PHP接口通过记录和查询 fpm_slow_log 可以分析耗时的方法。同时我们把慢执行数量进行监控,在新功能上线时需要密切关注。

  • 慢查询

无论是在生产环境还是压测过程,造成 MySQL 压力的很多情况是大量的慢查询,通过查 slow_log 或者从腾讯云管理后台可以进行慢查询分析。

很多资料都会提到使用索引来提升查询速度(先抛开I/O、锁这些不讲),然而即使有索引也可能踩坑。比如 “隐式类型转换” 的问题在腾讯问卷系统中就存在。

图 16

图 16

MySQL 会根据需要自动将数字转换为字符串,比如 openid 为 varchar 类型,当使用 where openid=123456 的查找时,实际进行了全表扫描,解决方案是查询前在代码中做转换,或者使用 CAST 方法转换。

2.2.2 合理使用组件

  • 是否有性能更高的替代

专业的事情交给专业的人干,在组件选型同样适用。在改造过程中,Elasticsearch 组件的引入,减少了在 MySQL 做搜索或聚合,提高了前面案例提到的打卡统计的计算能力。

  • 是否有更低成本的替代

Web 应用在使用缓存时,往往不注意容量和带宽的成本,回顾前面的压力测试,我们发现将大量的问卷静态数据存在 Redis 后,Redis 很快成为负载瓶颈,实际上只需要稍加改造,就可以找到合适的存储替代。

四、小结与展望

本文整理了 Web 应用高并发改造过程中的一些关注点,实际上每个加粗的小点都应该展开来更深入地探讨,提供对比方案和详细效果。但作为开发的我往往感觉到,自己不缺乏解决单个问题的能力,而缺乏系统性思维和全局观,本文也算是为高并发实践记录一些思路和方向,后续应继续补充完善。

回顾这段时间的改造效果,在使用资源不到两倍的情况下实现了5-10倍的负载能力,并且使继续扩容成为可能。虽然我们用效率最高的手段,让系统顺利支持了疫情期间的并发量,但如果要实现十倍甚至百倍的负载能力,当前的工作必定是不够的,或许更完善的分布式架构、或者当前火热的微服务架构,是我们系统下一步的考虑方向。